A pre-dev review of the balanced theme (the one the All-Screens canvas renders). Covers three things: colour contrast (computed), Dynamic Type / large-text behaviour, and VoiceOver / semantics. Scope note: these screens are a static design mock, so some items (true Dynamic Type scaling, live VoiceOver focus order) are implementation guidance for dev rather than something the mock itself proves. Concrete fixes already applied are marked ✅.
Computed against the live balanced-theme tokens. AA needs 4.5:1 for normal text, 3:1 for large text (≥24px, or ≥18.66px bold) and for UI component boundaries / meaningful graphics.
| Foreground → background | Ratio | Verdict |
|---|---|---|
ink #15140f → white (primary text) | 18.4:1 | ✅ AAA |
ink → off-white #f9f7f2 | 17.2:1 | ✅ AAA |
ink-2 #3a3833 → white (secondary) | 11.7:1 | ✅ AAA |
ink-2 → paper-2 card #f1ede6 | 10.0:1 | ✅ AAA |
ink-3 #524d44 → white (captions) | 8.4:1 | ✅ AAA |
ink-3 → paper-2 card | 7.2:1 | ✅ AAA |
accent #a32a18 → white (links/eyebrows) | 7.2:1 | ✅ AA |
accent → accent-soft tint | 6.1:1 | ✅ AA |
white → accent (button label) | 7.2:1 | ✅ AA |
white → ink (inverted banner/button) | 18.4:1 | ✅ AAA |
t-amber #6f5210 → white | 7.3:1 | ✅ AA |
t-amber → amber-soft | 6.1:1 | ✅ AA |
t-green #2f5a24 → white | 8.1:1 | ✅ AA |
t-green → green-soft | 6.7:1 | ✅ AA |
rule-2 #8a7f68 → white | 3.95:1 | ⚠️ text-only issue (see below) |
rule #e6ded2 → white (hairlines) | 1.3:1 | ✅ non-text divider (exempt) |
Headline: the palette is strong. Every text/icon colour that carries meaning clears AA, most by a wide margin — the warm-paper background does not hurt contrast because the ink ramp is genuinely dark.
The one finding — rule-2 as text. #8a7f68 is 3.95:1: perfectly fine for what it's mostly used for (disclosure chevrons, toggle-off track, unselected radio borders, step dots, drag handles — all UI components / decorative, where the bar is 3:1), but below 4.5:1 if used for readable text.
fc-auth.jsx Field) was rule-2 → now ink-3 (8.4:1). Placeholders read slightly darker, still clearly muted.rule-2 for borders/icons only; never set body or label text in it. If a future "disabled/ghost text" state is needed, use ink-3 or introduce a token at ≥4.5:1.OnWeight - Spec.html, … Flows.html, OnWeight - Logo by Sport.html) use a lighter rule-2 #b8ad97 (~2.3:1) for decorative section numbers. Cosmetic, internal docs, not the product — bump if you want them legible, otherwise ignore.Not an issue, but worth knowing: meaning is never carried by colour alone — pace verdicts pair the green/amber/red with words ("Made", "Behind"), and the week-streak dots pair colour with a "3 of 7" count. Good. Keep that discipline as new states are added.
All type in the mock is fixed px (hero numeral 104px, limit 64px, titles 24–28px, body 13–16px, mono eyebrows 10–10.5px). That's correct for a pixel-accurate design artifact, but it means the mock does not itself demonstrate Dynamic Type — that's the single biggest dev translation task here.
Findings / guidance for dev:
UIFontMetrics (or SwiftUI .font(.title) etc.); Android: sp units; web: rem + respect the OS font-scale. Tie each role (Hero / Title / Body / Caption / Eyebrow) to one scalable style so the whole app scales coherently.line-height: 0.8, letter-spacing: -0.035em, baseline-aligned next to the "KG" unit. Risks when text scales up or the value is wide: collide with the unit or clip at the 393px frame. Use a shrink-to-fit numeral (e.g. width-aware clamp() / minimumScaleFactor) so it never overflows.
line-height: 0.8 is safe for bare digits (no ascenders/descenders) but will clip if a glyph with a descender ever lands there — keep it digits-only, or raise to ≥0.9 when scaled.fontFeatureSettings: '"tnum"' on the data rows keeps numerals from reflowing as they change. Extend the same to the hero and trend labels.The mock uses real <button>s for most actions and pairs icons with text labels, which is a good start. The following are dev-implementation notes (a static mock can't fully encode focus order):
aria-hidden. Disclosure chevrons, dot indicators, drag handles, the bullet dots and most glyphs are decorative — hide them from the AT tree so VoiceOver doesn't announce "image" / empty groups. (fc-dash.jsx Disc already sets aria-hidden="true" — make that the rule for every non-meaningful SVG in the shared kit: Row chevron, Toggle, step dots, AvatarRow chevron.)aria-label (Back / Close). Apply the same to the gear/settings button, the paywall close, the number-pad keys, and tab-bar items (each tab needs a label + aria-current on the active one).aria-label="76.0 kilograms" and aria-hidden the blinking caret, so VoiceOver says "76.0 kilograms", not "76.0 … KG … (empty)".role="img" + an aria-label summarising it ("Trend: 78.4 to 76.0 kg over 9 days, on pace for 74.4"), and ideally a visually-hidden data table for the underlying points.aria-live="polite" region (assertive for errors) so they're spoken when they appear, not just seen.prefers-reduced-motion for any entrance/transition animation added to these screens (gate decorative motion; keep functional state changes instant).| Area | State |
|---|---|
| Contrast — text & UI | ✅ Passes AA across the board; placeholder fixed |
Contrast — rule-2 as text | ✅ Fixed (placeholder); keep rule-2 for borders/icons only |
| Dynamic Type wiring | 🟡 Dev task — map fixed px → scalable text styles, 11px floor |
| Hero numeral at large sizes | 🟠 Dev task — shrink-to-fit + cap growth so it never clips/overflows |
Decorative SVG aria-hidden | 🟡 Dev task — apply kit-wide (pattern set in fc-dash) |
| Icon-button / tab labels | 🟡 Dev task — extend the aria-label pattern already on back/close |
| Chart text alternatives | 🟠 Dev task — role="img" + summary + hidden data table |
| Live-region status | 🟠 Dev task — offline/error/sync banners |
| Touch targets | 🟡 Dev task — back/close 38→44pt hit area |
| Colour-only meaning | ✅ Already avoided |
Bottom line: the colour system is genuinely accessible — the warm palette costs nothing on contrast, and the one text-contrast slip (placeholder) is fixed. The remaining work is dev implementation, not redesign: wire Dynamic Type to scalable styles with a sensible floor and a capped hero, and add the standard semantic layer (aria-hidden on decorative art, labels on icon buttons/tabs, text alternatives for charts, live regions for status, 44pt targets).